using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.Xml.XPath;
using CriticalSystems.Instrumentation.Configuration;
using CriticalSystems.Instrumentation.LogWriters;

namespace CriticalSystems.Instrumentation.Internal
{
    public class Log
    {
        private static readonly ILogWriterFactory logWriterFactory;
        private static readonly IXPathNavigable logWriterSettings;
        private static readonly Trace trace = new Trace(typeof (Log).FullName);
        private readonly string assemblyName;
        private readonly string fullTypeName;
        private readonly string typeNamespace;
        private readonly ILogWriter logWriter;
        private readonly IInstrumentationConfiguration configuration;
        private static readonly IDictionary<Exception, ExceptionInfo> loggedExceptions = new Dictionary<Exception, ExceptionInfo>();

        static Log()
        {
            try
            {
                InstrumentationConfiguration configuration = InstrumentationConfiguration.Instance;
                WriterElement writerElement = configuration.LogWriter;
                string factoryTypeName = writerElement.Factory;
                if (!string.IsNullOrEmpty(factoryTypeName))
                {
                    logWriterFactory = CreateLogWriterFactory(factoryTypeName);
                    if (logWriterFactory == null)
                    {
                        throw new ConfigurationErrorsException(string.Format(Resource.Culture,
                                                                             Resource.InvalidLogWriterFactoryType,
                                                                             factoryTypeName));
                    }
                    logWriterSettings = writerElement.Settings == null ? null : writerElement.Settings.InnerXml;
                }
                else
                {
                    logWriterFactory = new EventLogWriterFactory();
                }
            }
            catch (Exception ex)
            {
                Trace.Error(".cctor", ex);
            }
        }

        static bool LoggedException(Exception exception, out ExceptionInfo exceptionInfo)
        {
            bool alreadyLogged = true;
            lock (loggedExceptions)
            {
                if (!loggedExceptions.TryGetValue(exception, out exceptionInfo))
                {
                    exceptionInfo = new ExceptionInfo(exception);
                    loggedExceptions.Add(exception, exceptionInfo);
                    alreadyLogged = false;
                }
            }
            return alreadyLogged;
        }

        public Log(string assemblyName, string fullTypeName)
        {
            this.assemblyName = assemblyName;
            this.fullTypeName = fullTypeName;
            this.configuration = InstrumentationConfiguration.Instance;
            this.typeNamespace = string.Empty;
            int index = this.fullTypeName.LastIndexOf(Type.Delimiter);
            if (index != -1)
            {
                this.typeNamespace = this.fullTypeName.Substring(0, index);
            }
            this.logWriter = logWriterFactory.Create(fullTypeName, logWriterSettings);
        }

        internal Log(string assemblyName, string fullTypeName, IInstrumentationConfiguration configuration)
            : this(assemblyName, fullTypeName)
        {
            this.configuration = configuration;
        }

        public static Trace Trace
        {
            get { return trace; }
        }

        private static ILogWriterFactory CreateLogWriterFactory(string factoryTypeName)
        {
            Type factoryType = Type.GetType(factoryTypeName, false, true);
            if (factoryType == null)
            {
                throw new ConfigurationErrorsException(string.Format(Resource.Culture,
                                                                     Resource.InvalidLogWriterFactoryType,
                                                                     factoryTypeName));
            }
            object instance = factoryType.Assembly.CreateInstance(factoryType.FullName, false,
                                                                  BindingFlags.Instance |
                                                                  BindingFlags.Public,
                                                                  null, null,
                                                                  CultureInfo.InvariantCulture,
                                                                  null);
            return instance as ILogWriterFactory;
        }

        public void In(string method)
        {
            try
            {
                BoundaryType boundaryType = GetBoundaryType();
                LoggingFlags logging = GetLoggingFlag(boundaryType, LoggingType.LogEntry);
                if ((this.configuration.Logging & logging) != 0)
                {
                    this.logWriter.LogIn(boundaryType, GetLogSource(method));
                }
            }
            catch (Exception ex)
            {
                Trace.Error("In", ex);
            }
        }

        public void Out(string method)
        {
            try
            {
                BoundaryType boundaryType = GetBoundaryType();
                LoggingFlags logging = GetLoggingFlag(boundaryType, LoggingType.LogExit);
                if ((this.configuration.Logging & logging) != 0)
                {
                    this.logWriter.LogOut(boundaryType, GetLogSource(method));
                }
            }
            catch (Exception ex)
            {
                Trace.Error("Out", ex);
            }
        }

        public void Out(string method, Exception ex)
        {
            try
            {
                BoundaryType boundaryType = GetBoundaryType();
                LoggingFlags logging = GetLoggingFlag(boundaryType, LoggingType.LogAbnormalExit);
                ExceptionInfo exceptionInfo;
                bool alreadyLogged = LoggedException(ex, out exceptionInfo);
                if ((this.configuration.Logging & logging) != 0 ||
                    (!alreadyLogged && (this.configuration.Logging & LoggingFlags.LogFirstAbnormalExit) != 0))
                {
                    this.logWriter.LogOut(boundaryType, GetLogSource(method), exceptionInfo);
                }
            }
            catch (Exception ex2)
            {
                Trace.Error("Out", ex2);
            }
        }

        public void Info(string method, string text, params object[] parameters)
        {
            try
            {
                if ((this.configuration.Logging & LoggingFlags.LogInformation) != 0)
                {
                    this.logWriter.LogInfo(GetLogSource(method),
                                           string.Format(CultureInfo.InvariantCulture, text, parameters));
                }
            }
            catch (Exception ex)
            {
                Trace.Error("Info", ex);
            }
        }

        public void Warning(string method, string text, params object[] parameters)
        {
            try
            {
                if ((this.configuration.Logging & LoggingFlags.LogWarning) != 0)
                {
                    this.logWriter.LogWarning(GetLogSource(method),
                                              string.Format(CultureInfo.InvariantCulture, text, parameters));
                }
            }
            catch (Exception ex)
            {
                Trace.Error("Warning", ex);
            }
        }

        public void Error(string method, string text, params object[] parameters)
        {
            try
            {
                if ((this.configuration.Logging & LoggingFlags.LogError) != 0)
                {
                    this.logWriter.LogError(GetLogSource(method),
                                            string.Format(CultureInfo.InvariantCulture, text, parameters));
                }
            }
            catch (Exception ex)
            {
                Trace.Error("Error", ex);
            }
        }

        public void Error(string method, Exception ex)
        {
            try
            {
                if ((this.configuration.Logging & LoggingFlags.LogError) != 0)
                {
                    ExceptionInfo exceptionInfo;
                    LoggedException(ex, out exceptionInfo);
                    this.logWriter.LogError(GetLogSource(method), exceptionInfo);
                }
            }
            catch (Exception ex2)
            {
                Trace.Error("Error", ex2);
            }
        }

        private BoundaryType GetBoundaryType()
        {
            BoundaryType boundaryType = BoundaryType.None;
            try
            {
                MethodBase callingMethod = ReflectionHelper.GetCallingMethod(true);
                if (callingMethod.DeclaringType.FullName == this.fullTypeName)
                {
                    boundaryType = BoundaryType.FunctionBoundary;
                }
                else
                {
                    if (callingMethod.DeclaringType.Namespace == this.typeNamespace)
                    {
                        boundaryType = BoundaryType.ComponentBoundary;
                    }
                    else
                    {
                        if (InstrumentationConfiguration.Instance.IsSubsystemNamespace(callingMethod.DeclaringType.Namespace))
                        {
                            if (InstrumentationConfiguration.Instance.IsSubsystemNamespace(this.typeNamespace))
                            {
                                boundaryType = BoundaryType.ModuleBoundary;
                            }
                            else
                            {
                                boundaryType = BoundaryType.ExternalModuleBoundary;
                            }
                        }
                        else
                        {
                            if (callingMethod.DeclaringType.Namespace == "System.Threading")
                            {
                                boundaryType = BoundaryType.ThreadBoundary;
                            }
                            else
                            {
                                boundaryType = BoundaryType.SubsystemBoundary;
                            }
                        }
                    }
                }                
            }
            catch (Exception ex)
            {
                 trace.Error("GetBoundaryType", ex);   
            }
            return boundaryType;
        }

        private LogSource GetLogSource(string method)
        {
            LogSource source = new LogSource();
            source.Subsystem = this.configuration.Subsystem;
            source.Module = this.assemblyName;
            source.Component = this.fullTypeName;
            source.Function = method;
            source.Process = GetProcessInformation();
            source.Thread = GetThreadInformation();
            source.Identity = GetIdentityInformation();
            return source;
        }

        private static string GetIdentityInformation()
        {
            return Environment.UserDomainName + @"\" + Environment.UserName;
        }

        private static string GetThreadInformation()
        {
           Thread thread = Thread.CurrentThread;
            string threadInfo = thread.ManagedThreadId.ToString();
            if (string.IsNullOrEmpty(thread.Name))
            {
                threadInfo += " (" + thread.Name + ")";
            }
            if (thread.IsThreadPoolThread)
            {
                threadInfo += " from thread pool";                
            }
            return threadInfo;
        }

        private static string GetProcessInformation()
        {
            int processId = Process.GetCurrentProcess().Id;
            string processName = Process.GetCurrentProcess().Modules[0].ModuleName;
            string appDomainName = AppDomain.CurrentDomain.FriendlyName;
            return string.Format(CultureInfo.InvariantCulture, "{0} ({1}) - domain {2}", processId, processName,
                                 appDomainName);
        }

        private static ExceptionInfo GetExceptionInfo(Exception ex)
        {
            ExceptionInfo exceptionInfo = new ExceptionInfo(ex);
            return exceptionInfo;
        }

        private static LoggingFlags GetLoggingFlag(BoundaryType boundaryType, LoggingType type)
        {
            int flag = ConvertLoggingActionToFlag(type);
            int shift = 10;
            switch (boundaryType)
            {
                case BoundaryType.SubsystemBoundary:
                    shift = 2;
                    break;
                case BoundaryType.ThreadBoundary:
                    shift = 3;
                    break;
                case BoundaryType.ModuleBoundary:
                    shift = 4;
                    break;
                case BoundaryType.ExternalModuleBoundary:
                    shift = 5;
                    break;
                case BoundaryType.ComponentBoundary:
                    shift = 6;
                    break;
                case BoundaryType.FunctionBoundary:
                    shift = 7;
                    break;
            }
            return (LoggingFlags)(flag << (shift * 4));
        }

        internal static int ConvertLoggingActionToFlag(LoggingType type)
        {
            int flag = 0;
            switch (type)
            {
                case LoggingType.LogEntry:
                    flag = 1;
                    break;
                case LoggingType.LogExit:
                    flag = 2;
                    break;
                case LoggingType.LogAbnormalExit:
                    flag = 4;
                    break;
            }
            return flag;
        }
    }
}